package ru.amse.podolsky.fractalarch.model;

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author Алексей
 */
//package ru.amse.podolsky.fractalarch;

import java.awt.image.*;
import java.io.*;

public class Compression implements ICompression{

   //Constants
    private static final int blockSize = IParams.BLOCK_SIZE;
    private static final int domensPerBlock = 4;
    private static final int domenSize = IParams.DOMEN_SIZE;
    private static final int numberOfAfinConversions = 8;
    private static final int numberOfDecompressTurns = 16;
    private static final int maxPixelIntensiv = 255;
    private static final int zeroCharCode = 48;
    
   private final BufferedImage buff;
   
   private final Block[][] bloks;
   
   private final int imageWidth;
   private final int imageHeight;
   private final int imageWidthInBlocks;
   private final int imageHeightInBlocks;
   private final int numberOfDomens;
   private final byte domensPositionFlag;
   
   public Compression(BufferedImage buff, String step) throws IOException {
        this.buff = buff;
        
        this.imageWidth = buff.getWidth();
        this.imageHeight = buff.getHeight();
                       
        //Измеряется и запоминается размеры изображения в блоках

        BlockLineParams params = new BlockLineParams();
        params = params.getImageLineParamsInBlocks(this.buff, toInt(step));
        
        this.imageWidthInBlocks = params.imageWidthInBlocks;
        this.imageHeightInBlocks = params.imageHeightInBlocks;        

        //Запуск формирования массива блоков

        this.bloks = blockFormation(this.buff, toInt(step));

        //Определяем параметры разбиеня на домены

        this.numberOfDomens = getSizeInDomens();
        this.domensPositionFlag = defineDomensPositionFlag();
   }

   //Возвращает 0, если размеры картинки кратны размеру домена, 1 - если ширина не кратна размеру
   //блока и блок при этом залазит только на один домен соседа слева, 2 - если ширина не кратна размеру
   //блока и блок при этом залазит на два домена соседа слева, 3 - если не кратна высота и блок залезает
   //только на один домен соседа сверху, 4 - если не кратна высота и блок залезает на два домена соседа
   //сверху, 5 - 1 + 3, 6 - 1 + 4, 7 - 2 + 3, 8 - 2 + 4

   private byte defineDomensPositionFlag() {
       //Смотрим, есть ли у нас блоки, целиком не поместившиеся на картинку, и выставляем соответствующий
       //флаг. Здесь используется то, что в блоке - 4 домена!!!!

       int horizontalSpace = IParams.BLOCK_SIZE - (this.bloks[this.imageWidthInBlocks - 1][0].getX() - this.bloks[this.imageWidthInBlocks - 2][0].getX());
       int verticalSpace = IParams.BLOCK_SIZE - (this.bloks[0][this.imageHeightInBlocks - 1].getY() - this.bloks[0][this.imageHeightInBlocks - 2].getY());
       byte halfBlock = IParams.BLOCK_SIZE / 2;

       if(this.numberOfDomens == (this.imageHeight / IParams.DOMEN_SIZE)*(this.imageWidth / IParams.DOMEN_SIZE)){
           return 0;
       }
       if(verticalSpace > halfBlock && horizontalSpace > halfBlock){
           return 8;
       }
       if(verticalSpace > halfBlock && horizontalSpace < halfBlock){
           return 7;
       }
       if(verticalSpace < halfBlock && horizontalSpace > halfBlock){
           return 6;
       }
       if(verticalSpace < halfBlock && horizontalSpace < halfBlock){
           return 5;
       }
       if(verticalSpace > halfBlock && horizontalSpace == 0){
           return 4;
       }
       if(verticalSpace < halfBlock && horizontalSpace == 0){
           return 3;
       }
        if(verticalSpace == 0 && horizontalSpace > halfBlock){
           return 2;
       }
       return 1;
   }
   
   private int getSizeInDomens(){
       //Считаем колличество доменов, помещающихся на картинке целиком
       int result = (this.imageHeight / IParams.DOMEN_SIZE)*(this.imageWidth / IParams.DOMEN_SIZE);
       //Если есть только такие домены, возвращаем их колличество
       if((this.imageHeight % IParams.DOMEN_SIZE == 0) && (this.imageWidth % IParams.DOMEN_SIZE == 0)){
           return result;
       }
       //В противном случае добавляем к общему колличеству блоков по
       // IParams.BLOCK_SIZE / IParams.DOMEN_SIZE доменов
       if (this.imageHeightInBlocks * IParams.BLOCK_SIZE > this.imageHeight){
           result += (this.imageHeight / IParams.DOMEN_SIZE)*(IParams.BLOCK_SIZE / IParams.DOMEN_SIZE);
       }
       if (this.imageWidthInBlocks * IParams.BLOCK_SIZE > this.imageWidth){
           result += (this.imageWidth / IParams.DOMEN_SIZE)*(IParams.BLOCK_SIZE / IParams.DOMEN_SIZE);
       }
       if ((this.imageHeightInBlocks * IParams.BLOCK_SIZE > this.imageHeight) &&
               (this.imageWidthInBlocks * IParams.BLOCK_SIZE > this.imageWidth)){
           result += domensPerBlock;
       }
       return result;
   }
   
   private Block[][] blockFormation(BufferedImage image, int step){
        
       int numberOfBlocksW = 0;
       int numberOfBlocksH = 0;
       int nonCrossedBlocksW = 0;
       int nonCrossedBlocksH = 0;        
       
       int width = image.getWidth() - blockSize;
       int height = image.getHeight() - blockSize;
       if(step > blockSize){
           step = blockSize;
       }       
       
       //Здесь рассчитывается количество блоков на которые надо разбить картинку.

       if(width % step == 0){
            numberOfBlocksW = width / step + 1;
            nonCrossedBlocksW = numberOfBlocksW;
       }else{
           numberOfBlocksW = width / step + 2;
           nonCrossedBlocksW = numberOfBlocksW - 1;
       }
       if(height % step == 0){
           numberOfBlocksH = height / step + 1;
           nonCrossedBlocksH = numberOfBlocksH;
       }else{
           numberOfBlocksH = height / step + 2;
           nonCrossedBlocksH = numberOfBlocksH - 1;
       }
       
       Block[][] localBlocks = new Block[numberOfBlocksW][numberOfBlocksH];
              
       //Здесь идет формирование массива блоков по вычисленным координатам
       //Три условия - необходимость формирования блоков на краю изображения 
       //в картинках, размеры которых не кратны размеру блока.

       for(int i = 0; i < nonCrossedBlocksW; ++i){
            for(int k = 0; k < nonCrossedBlocksH; ++k){
               localBlocks[i][k] = new Block(image, i * step, k * step); 
            }
        }
        
        if(nonCrossedBlocksW != numberOfBlocksW){
            for(int i = 0; i < nonCrossedBlocksH; ++i){
                localBlocks[nonCrossedBlocksW][i] = new Block(image, image.getWidth() - blockSize, i * step);
            }
        }
        if(nonCrossedBlocksH != numberOfBlocksH){
            for(int i = 0; i < nonCrossedBlocksW; ++i){
                localBlocks[i][nonCrossedBlocksH] = new Block(image, i * step, image.getHeight() - blockSize);
            }
        }
        if(nonCrossedBlocksH != numberOfBlocksH && nonCrossedBlocksW != numberOfBlocksW){
           localBlocks[nonCrossedBlocksW][nonCrossedBlocksH] = new Block(image, image.getWidth() - blockSize, image.getHeight() - blockSize); 
        }
       
       return localBlocks;
   }
   
   public int numberOfBlocks(){       
       return this.imageWidthInBlocks * this.imageHeightInBlocks;
   }
   
   public Block getBlock(int x, int y){
       return this.bloks[x][y];
   }
   
   public Block getBlock(int number){
       return this.getBlock(number % this.imageWidthInBlocks, number / this.imageWidthInBlocks);
   }
   
   public int getMiddleColor(){
       int color = 0;
       for(int i = 0; i < this.imageWidth; ++i){
           for(int k = 0; k < this.imageHeight; ++k){
               color += Math.abs(this.buff.getRGB(i, k) & maxPixelIntensiv);
           }
       }
       return color/(this.imageWidth*this.imageHeight);
   }
   
   public int getNumberOfDomens(){
       return this.numberOfDomens;
   }
   
   public int getImageWidth(){
       return this.imageWidth;
   }
   
   public int getImageHeight(){
       return this.imageHeight;
   }
   
   public DomenParams[] compress() throws IOException{
            int numberOfBlocks = this.numberOfBlocks();
            
            Domen[] domens= new Domen[this.numberOfDomens];
            for (int i = 0; i < domens.length; i++) {
                domens[i] = new Domen();
            }
        
            //Формирование из массива блоков - массива доменов
            //В зависимости от значения domensPositionFlag номера доменов
            //будут рассчитываться по разному

            int num = 0;
            for(int i = 0; i < numberOfBlocks; ++i){
                Domen[] dom = this.getBlock(i).getDomens();
                for (int j = 0; j < dom.length; j++) {
                    num = Domen.getDomenNumber(dom[j], this.domensPositionFlag, this.imageHeight, this.imageWidth);
                    if(num != -1){
                        domens[num] = dom[j];
                    }else{
                        throw new IOException("There is exception in domen formation. May be archiv had been damaged.");
                    }
                }
            }

           // int total =  this.numberOfDomens * numberOfBlocks;
            int count = 0;
            
            //Перебор всех доменов

            for(int i= 0; i < this.numberOfDomens; ++i){
                
                //Для каждого домена перебираются все блоки.

                for(int k = 0; k < numberOfBlocks; ++k){
                   count++;
                   //float a= (float)(count*1.0)/total;
                   //System.out.print(a+"\r");

                   //Для одного блока формируется из него массив
                   //доменов, поученных сжатием и афинными преобразованиями этого блока.

                   Domen[] currents = new Domen[numberOfAfinConversions];
                    //Проверяется, правильно ли сделал афинные преобразования.
                   //Сейчас написаны "оригинальные"

//                    for (int n = 0; n < 4; ++n){
//                        int num = n * 4;
//                        if(n == 0){
//                            currents[num] = this.getBlock(k).compress();
//                        }else{
//                            currents[num] = currents[num - 4].turn();
//                        }
//                        currents[num + 1] = currents[num].mirrorHorizontal();
//                        currents[num + 2] = currents[num].mirrorVertical();
//                        currents[num + 3] = currents[num + 1].mirrorVertical();
//                    }
                    
                        currents[0] = this.getBlock(k).compress();
                        currents[1] = currents[0].mirrorVertical();
                        currents[2] = currents[0].mirrorHorizontal();
                        currents[3] = currents[0].mirrorPrimDiag();
                        currents[4] = currents[0].mirrorSecDiag();
                        currents[5] = currents[0].turn();
                        currents[6] = currents[0].turn();
                        currents[7] = currents[0].turn();

                      //Проверка правильности афинных преобразований
//                    if(i == 0){
//                        GraphicOutput.drowToFile(currents, k);
//                    }
                    
                    //Для каждого получившегося в результате сжатия домена считается разность 
                    //между ним и доменом, афинно не преобразованным.

                    int delta = 0;
                    
                    for(int x = 0; x < domenSize; ++x){
                        for(int y = 0; y < domenSize; ++y){                            
                            delta += domens[i].getPixel(x, y) - currents[0].getPixel(x, y);                            
                        }
                    }
                    
                    //Осуществляется сдвиг по интенсивности

                    delta /= domenSize * domenSize;
                    
                    for(int x = 0; x < domenSize; ++x){
                        for(int y = 0; y < domenSize; ++y){
                            for(int n = 0; n < numberOfAfinConversions; n++){
                                int pixel = currents[n].getPixel(x, y) + delta;
                                if(pixel > this.maxPixelIntensiv){
                                pixel = this.maxPixelIntensiv;
                                }else{
                                    if(pixel  < 0){
                                        pixel = 0;
                                    }
                                }
                                currents[n].setPixel(x, y, pixel);
                            }
                        }
                    }

                    //Подсчет разности по интенсивности между сжатым блоком
                    //и исходным доменом

                    int deltaS = 0;
                    for(int n = 0; n < numberOfAfinConversions; ++n){
                        for(int x = 0; x < domenSize; ++x){
                            for(int y = 0; y < domenSize; ++y){
                                deltaS += Math.abs(domens[i].getPixel(x, y) 
                                    - currents[n].getPixel(x, y));    
                            }
                        }

                    //Внесение, если необходимо, корректировки в набор выходных
                    //параметров, предназначенных для записи в архив.

                    if(deltaS < domens[i].domenParams.testDelta){
                        domens[i].domenParams.sameBlockNumber = k;
                        domens[i].domenParams.shift = delta;
                        domens[i].domenParams.numberOfConversion = n;
                        domens[i].domenParams.testDelta = deltaS;
                    }
                    /*
                    if(deltaS < 64){
                        n = this.numberOfAfinConversions;
                    }
                    */
                    deltaS = 0;
                        
                    }
                }
            }
            
            DomenParams[] domensParam = new DomenParams [this.numberOfDomens];
            for(int i = 0; i < this.numberOfDomens; ++i){
                domensParam[i] = new DomenParams();
            }

            //Формирование выходного массив параметров.

            for(int i = 0; i < this.numberOfDomens; ++i){
                domensParam[i].domenXCoord = domens[i].domenParams.domenXCoord;
                domensParam[i].domenYCoord = domens[i].domenParams.domenYCoord;
                domensParam[i].sameBlockNumber = domens[i].domenParams.sameBlockNumber;
                domensParam[i].shift = domens[i].domenParams.shift;
                domensParam[i].numberOfConversion = domens[i].domenParams.numberOfConversion;
                domensParam[i].testDelta = domens[i].domenParams.testDelta;
            }
            
            return domensParam;
   }
   
   public BufferedImage decompress(DomenParams[] domenParams, int step){
       BufferedImage out = this.buff;
              
       BlockLineParams imageLineParams = new BlockLineParams();
       imageLineParams = imageLineParams.getImageLineParamsInBlocks(out, step);

       //Цикл по проходам при декомпресси

       for(int n = 0; n < numberOfDecompressTurns; n++){
           BufferedImage currentImage = out;
           Block[][] localBlocks = blockFormation(currentImage, step);
           
           //По всем доменам в изображении.

           for(int i = 0; i < this.numberOfDomens; ++i){
               int xcoord = domenParams[i].domenXCoord;
               int ycoord = domenParams[i].domenYCoord;
               
               int xBlockCoord = domenParams[i].sameBlockNumber % imageLineParams.imageWidthInBlocks;
               int yBlockCoord = domenParams[i].sameBlockNumber / imageLineParams.imageWidthInBlocks;
               
               Domen current = localBlocks[xBlockCoord][yBlockCoord].compress();
               int cur = domenParams[i].numberOfConversion;
               
               for(int x = 0; x < domenSize; ++x){
                   for(int y = 0; y < domenSize; ++y){
                       int color = current.getPixel(x, y) + domenParams[i].shift;
                       /*
                       if(color > this.maxPixelIntensiv){
                           color = this.maxPixelIntensiv;
                       }else{
                           if(color < 0){
                               color = 0;
                           }
                       }
                       */
                       current.setPixel(x, y, color);
                   }
               }
               //System.out.println(xcoord + "; " + ycoord + "; " + "color: " +current.getMediumColor());

               //Здесь - исходя из компрессии(афинных преобразований в ней)
//               while(cur / (this.numberOfAfinConversions / 4) > 0){
//                   current = current.turn();
//                   cur /= (this.numberOfAfinConversions / 4);
//               }
//               switch (cur){
//                   case 1:{current = current.mirrorHorizontal(); break;}
//                   case 2:{current = current.mirrorVertical(); break;}
//                   case 3:{current = current.mirrorHorizontal(); current = current.mirrorVertical(); break;}
//               }
               switch (cur){
                   case 1:{current = current.mirrorVertical(); break;}
                   case 2:{current = current.mirrorHorizontal(); break;}
                   case 3:{current = current.mirrorPrimDiag(); break;}
                   case 4:{current = current.mirrorSecDiag(); break;}
                   case 5:{current = current.turn(); break;}
                   case 6:{current = current.turn(); current.turn(); break;}
                   case 7:{current = current.turn(); current.turn(); current.turn(); break;}
               }
               
               //Запись изображения непосредственно на картинку.

               for(int x = 0; x < domenSize; ++x){
                   for(int y = 0; y < domenSize; ++y){
                       byte c = (byte)current.getPixel(x, y);
                       //System.out.print("c="+c);
                       int color = (c & 0xFF)<<16;
                       color += (c & 0xFF)<<8;
                       color += (c & 0xFF);
                       //System.out.println(" color="+color);
                       currentImage.setRGB(xcoord + x, ycoord + y, color);
                   }
               }
/*
               ImagesIO tmpIO = new ImagesIO("a_"+n+".bmp");
               try{
                tmpIO.write(currentImage);
               }
               catch(IOException e){}
*/
           }
           out = currentImage;
       }
       
       return out;
   }
   
   private int toInt(String string){
       int length = string.length();
       int out = 0;
       for(int i = 0; i < length; ++i){
           out =  out * 10 + ((int)string.charAt(i) - zeroCharCode) ;          
       }
       return out;
   }

   public BufferedImage returnBuff(){
       return this.buff;
   }
}
